﻿/* **********************************************************************************
 * Copyright (c) 2011 John Hughes
 *
 * Elve Touch is licenced under the Microsoft Reciprocal License (Ms-RL).
 *
 * Project Website: http://elvetouch.codeplex.com/
 * **********************************************************************************/

using System;
using System.Collections.Generic;
using System.Text;
using System.Net.Sockets;
using System.Net;
using System.Threading;
using System.IO;
using System.Drawing;
using System.Windows.Forms;
using System.Drawing.Imaging;

namespace ElveTouch
{
    public class TouchTcpAuthenticationResultReceivedEventArgs : EventArgs
    {
        public TouchServiceTcpCommunicationAuthenticationResults Result;
        public Size TouchScreenSize;

        public TouchTcpAuthenticationResultReceivedEventArgs(TouchServiceTcpCommunicationAuthenticationResults result, Size touchScreenSize)
        {
            Result = result;
            TouchScreenSize = touchScreenSize;
        }
    }

    public class TouchTcpClientStateChangedEventArgs : EventArgs
    {
        public TouchTcpClientState State;

        public TouchTcpClientStateChangedEventArgs(TouchTcpClientState state)
        {
            State = state;
        }
    }

    public class TouchTcpClientExceptionEventArgs : EventArgs
    {
        public string Message;

        public TouchTcpClientExceptionEventArgs(string message)
        {
            Message = message;
        }
    }
    

    public enum TouchTcpClientState
    {
        AttemptingToConnect,
        Authenticating,
        Authenticated,
        FailedAuthentication
    }

    public class TouchTcpClient : BinaryTcpCommunication
    {
        string _userName;
        string _password;
        TouchScreenInterface _form;
        Size _formSize; // used to prevent need for Control.Invoke to prevent cross threading exception in ConnectionEstablished event.
        int _pixelDepthBitCount;
        bool _supportsAlphaChannel;
        TouchTcpClientState _state;

        public event EventHandler<TouchTcpClientStateChangedEventArgs> TouchTcpClientStateChanged;
        public event EventHandler<TouchTcpAuthenticationResultReceivedEventArgs> TouchTcpAuthenticationResultReceived;
        public new event EventHandler<EventArgs> ConnectionEstablished;
        public new event EventHandler<EventArgs> ConnectionLost;
        public event EventHandler<TouchTcpClientExceptionEventArgs> TouchTcpClientException;
        public event EventHandler<EventArgs> TouchTcpClientScreenReceived;

        DrawingCommandQueue _drawingCommandsQueue;

        public RenderingMode RenderingMode;

        public TouchTcpClient(string hostName, int port, string userName, string password, TouchScreenInterface form, RenderingMode renderingMode, int pixelDepthBitCount, bool supportsAlphaChannel)
            : base(hostName, port)
        {
            _userName = userName;
            _password = password;
            _form = form;
            _formSize = form.Size;
            RenderingMode = renderingMode;
            _pixelDepthBitCount = pixelDepthBitCount;
            _supportsAlphaChannel = supportsAlphaChannel;
        }

        /// <summary>
        /// Asynchronously attempts to connect and enabled connection monitoring.
        /// </summary>
        public new void Open()
        {
            OnTouchTcpClientStateChanged(TouchTcpClientState.AttemptingToConnect);

            base.MessageReceived += new EventHandler<MessageReceivedEventArgs>(_tcp_MessageReceived);

            // Set up connection monitor.
            base.ConnectionMonitorTimeout = 30000;
            base.ConnectionEstablished += new EventHandler<EventArgs>(_tcp_ConnectionEstablished);
            base.ConnectionLost += new EventHandler<EventArgs>(tcp_ConnectionLost);
            base.ConnectionMonitorTest += new EventHandler(_tcp_ConnectionMonitorTest);
            base.StartConnectionMonitor();

            _drawingCommandsQueue = new DrawingCommandQueue(_form, this);
            _drawingCommandsQueue.NamedImageRequestTimeout += new EventHandler(_drawingCommandsQueue_NamedImageRequestTimeout);
        }

        void _drawingCommandsQueue_NamedImageRequestTimeout(object sender, EventArgs e)
        {
            // Drop the connection and reconnect.
            base.OnConnectionLost();
        }

        void _tcp_ConnectionMonitorTest(object sender, EventArgs e)
        {
            BinaryTcpCommunication tcp = (BinaryTcpCommunication)sender;

            // Send Ping (we do it here so that he header's sequence number is correct).
            tcp.SendMessage((byte)TouchServiceTcpCommunicationPayloadTypes.Ping, new byte[0]);
        }

        void _tcp_ConnectionEstablished(object sender, EventArgs e)
        {
            // Initiate authentication.

            System.Diagnostics.Debug.WriteLine(DateTime.Now.ToString() + "  **** TouchClient %%%%%%%%%%%%%%%%%% ConnectionEstablished.  Sending HELLO command.");

            OnTouchTcpClientStateChanged(TouchTcpClientState.Authenticating);

            // Send the size of the form at the time of the connection. If the user changes to/from windowed mode the app can deal with the change as it likes, such as reconnecting and sending the new size, or scaling or scrolling the window, etc.
            TouchClientAppID appID = TouchClientAppID.WindowsMacLinuxPC;
            string deviceID = Environment.MachineName;

            HelloPayload hello = new HelloPayload(1, appID, RenderingMode, _formSize, _pixelDepthBitCount, _supportsAlphaChannel, deviceID);

            base.SendMessage(hello);

            OnConnectionEstablished();
        }

        void tcp_ConnectionLost(object sender, EventArgs e)
        {
            // NOTE: The client will keep attempting to reconnect.

            System.Diagnostics.Debug.WriteLine(DateTime.Now.ToString() + "  **** TouchClient %%%%%%%%%%%%%%%%%% Connection Lost.  The client will keep attempting to reconnect.");

            OnTouchTcpClientStateChanged(TouchTcpClientState.AttemptingToConnect);

            OnConnectionLost();
        }

        public TouchTcpClientState CurrentState
        {
            get { return _state; }
        }

        public string DrawingQueueState
        {
            get
            {
                return _drawingCommandsQueue.DrawingState;
            }
        }

        public int DrawingQueueItemCount
        {
            get { return _drawingCommandsQueue.Count; }
        }



        void _tcp_MessageReceived(object sender, MessageReceivedEventArgs e)
        {
            System.Diagnostics.Debug.WriteLine(DateTime.Now.ToString() + "  **** TouchClient Received Command : " + ((TouchServiceTcpCommunicationPayloadTypes)e.PayloadType).ToString());

            try
            {
                switch ((TouchServiceTcpCommunicationPayloadTypes)e.PayloadType)
                {
                    case TouchServiceTcpCommunicationPayloadTypes.Ping:
                        base.SendMessage((byte)TouchServiceTcpCommunicationPayloadTypes.Pong, null);
                        break;

                    case TouchServiceTcpCommunicationPayloadTypes.Pong:
                        break; // do nothing. It's just a response to Ping.

                    case TouchServiceTcpCommunicationPayloadTypes.Disconnect:
                        //TODO...
                        break;

                    case TouchServiceTcpCommunicationPayloadTypes.Error:

                        // The server will automatically disconnect after sending the error to us.
                        ErrorPayload errorPayload = new ErrorPayload(e.Payload);

                        System.Diagnostics.Debug.WriteLine(DateTime.Now.ToString() + "  **** Error Command. Message: " + errorPayload.Message);

                        OnTouchTcpClientException(errorPayload.Message);
                        break;

                    case TouchServiceTcpCommunicationPayloadTypes.AuthenticateChallenge:

                        AuthenticationChallengePayload challenge = new AuthenticationChallengePayload(e.Payload);

                        // Create new random token
                        byte[] t2 = Guid.NewGuid().ToByteArray();

                        // Calculate password hash.
                        System.Security.Cryptography.HashAlgorithm alg = System.Security.Cryptography.HashAlgorithm.Create("MD5");
                        byte[] passwordHash = alg.ComputeHash(UTF8Encoding.UTF8.GetBytes(_password));

                        // Concatenate the byte arrays for hashing
                        byte[] hashMe = SystemUtility.ConcatenateByteArrays(t2, passwordHash, challenge.AuthenticationToken);

                        // Calculate the hash
                        System.Security.Cryptography.MD5 sha = System.Security.Cryptography.MD5.Create();
                        byte[] helloHash = sha.ComputeHash(hashMe);

                        AuthenticatePayload auth = new AuthenticatePayload(_userName, t2, helloHash);

                        base.SendMessage(auth);
                        break;

                    case TouchServiceTcpCommunicationPayloadTypes.AuthenticationResult:

                        AuthenticationResultPayload authResult = new AuthenticationResultPayload(e.Payload);


                        if (authResult.AuthenticationResult == TouchServiceTcpCommunicationAuthenticationResults.Success)
                        {
                            OnTouchTcpClientStateChanged(TouchTcpClientState.Authenticated);

                            OnTcpAuthenticationResultReceived(authResult.AuthenticationResult, authResult.TouchScreenSize);
                        }
                        else
                        {
                            OnTouchTcpClientStateChanged(TouchTcpClientState.FailedAuthentication);

                            OnTcpAuthenticationResultReceived(authResult.AuthenticationResult, Size.Empty);
                        }

                        break;

                    case TouchServiceTcpCommunicationPayloadTypes.NamedImage:
                        // Don't put this in the queue

                        RendererNamedImage namedImage = new RendererNamedImage(e.Payload);

                        System.Diagnostics.Debug.WriteLine(DateTime.Now.ToString() + "  **** TouchClient Received named image: " + namedImage.ImagePath + ", hash: " + namedImage.ImageHash);

                        // Add/replace the image to the image cache.
                        ImageCache.Add(namedImage.ImagePath, namedImage.ImageHash, namedImage.ImageBytes);

                        // Let the worker thread know that it can start processing... the first item should be a DrawNamedImage request.
                        _drawingCommandsQueue.NamedImageReceived(namedImage.ImagePath);
                        break;

                    case TouchServiceTcpCommunicationPayloadTypes.ChangeScreenMode:

                        RendererChangeScreenMode changeScreenMode = new RendererChangeScreenMode(e.Payload);

                        switch (changeScreenMode.ScreenMode)
                        {
                            case ScreenMode.ToggleFullscreen:
                                _form.ToggleFullscreen();
                                break;
                        }
                        break;

                    case TouchServiceTcpCommunicationPayloadTypes.Exit:
                        Application.Exit(); // TODO: this may not exit??? Maybe I should call Environment.Exit
                        break;

                    default:
                        // Add drawing commands to the queue.
                        if (_drawingCommandsQueue != null)
                            _drawingCommandsQueue.Enqueue(e);
                        break;
                }
            }
            catch (Exception ex)
            {
                #if DEBUG
                System.Diagnostics.Debugger.Break();
                #endif

                //Logger.Log("An unexpected error occurred while processing the response: " + e.Payload + "\r\n" + Utility.GetExceptionMessages(ex), LoggerSeverity.Error);

                //_tcp.SendErrorMessage(messageID, e.Payload, Utility.GetExceptionMessages(ex));
            }
        }

        protected new void OnConnectionEstablished() // use "new" keyword here! "override" cause it not to work when running!
        {
            try
            {
                if (ConnectionEstablished != null)
                    ConnectionEstablished(this, new EventArgs());
            }
            catch
            {
            }
        }

        protected new void OnConnectionLost() // use "new" keyword here! "override" cause it not to work when running!
        {
            try
            {
                if (ConnectionLost != null)
                    ConnectionLost(this, new EventArgs());
            }
            catch
            {
            }
        }


        protected virtual void OnTcpAuthenticationResultReceived(TouchServiceTcpCommunicationAuthenticationResults authResult, Size touchScreenSize)
        {
            if (TouchTcpAuthenticationResultReceived != null)
            {
                try
                {
                    TouchTcpAuthenticationResultReceivedEventArgs e = new TouchTcpAuthenticationResultReceivedEventArgs(authResult, touchScreenSize);
                    TouchTcpAuthenticationResultReceived(this, e);
                }
                catch
                {
                    // just catch the error in the user code and continue.
                }
            }
        }

        protected virtual void OnTouchTcpClientStateChanged(TouchTcpClientState state)
        {
            _state = state;

            if (TouchTcpClientStateChanged != null)
            {
                try
                {
                    TouchTcpClientStateChangedEventArgs e = new TouchTcpClientStateChangedEventArgs(state);
                    TouchTcpClientStateChanged(this, e);
                }
                catch
                {
                    // just catch the error in the user code and continue.
                }
            }
        }



        protected virtual void OnTouchTcpClientException(string message)
        {
            if (TouchTcpClientException != null)
            {
                try
                {
                    TouchTcpClientExceptionEventArgs e = new TouchTcpClientExceptionEventArgs(message);
                    TouchTcpClientException(this, e);
                }
                catch
                {
                    // just catch the error in the user code and continue.
                }
            }
        }


        protected virtual void OnTouchTcpClientScreenReceived()
        {
            if (TouchTcpClientScreenReceived != null)
            {
                try
                {
                    TouchTcpClientScreenReceived(this, new EventArgs());
                }
                catch
                {
                    // just catch the error in the user code and continue.
                }
            }
        }

        public override void Dispose()
        {
            try
            {
                if (_drawingCommandsQueue != null)
                {
                    _drawingCommandsQueue.Dispose();
                    _drawingCommandsQueue = null;
                }
            }
            catch
            {
            }

            try
            {
                base.Dispose();
            }
            catch
            {
            }
        }

        internal void TouchEvent(TouchEventType eventType, int x, int y)
        {
            TouchEventPayload touchEvent = new TouchEventPayload(eventType, new Point(x, y));
            base.SendMessage(touchEvent);
        }

        internal void LocationChangeEvent(double latitude, double longitude, double altitude, double course, double speedMetersPerSecond)
        {
            LocationChangeEventPayload locationChangedEvent = new LocationChangeEventPayload(latitude, longitude, altitude, course, speedMetersPerSecond);
            base.SendMessage(locationChangedEvent);
        }

        internal void RequestSnapshot(Rectangle bounds)
        {
            RequestSnapshotPayload requestSnapshot = new RequestSnapshotPayload(bounds);
            base.SendMessage(requestSnapshot);
        }

        internal void RequestRedraw(Rectangle bounds)
        {
            RequestRedrawPayload requestRedraw = new RequestRedrawPayload(bounds);
            base.SendMessage(requestRedraw);
        }
    }

    public class DrawingCommandQueue : IDisposable
    {
        Queue<MessageReceivedEventArgs> _queue = new Queue<MessageReceivedEventArgs>();

        List<Rectangle> _screenChanges = new List<Rectangle>();

        AutoResetEvent _event = new AutoResetEvent(false);

        bool _canceled;

        TouchScreenInterface _form;

        TouchTcpClient _tcp;

        string _requestedImagePath;

        System.Threading.Timer _namedImageRequestTimer;
        public event EventHandler NamedImageRequestTimeout;

        System.Threading.Timer _screenChangeTimer;


        protected class clippingCommand
        {
            public Region PreviousClip;
            public PushClippingAreaMode Mode;
            public Rectangle Bounds;
            public int XRounding;
            public int YRounding;

            public clippingCommand(Region previousClip, PushClippingAreaMode mode, Rectangle bounds, int xRounding, int yRounding)
            {
                PreviousClip = previousClip;
                Mode = mode;
                Bounds = bounds;
                XRounding = xRounding;
                YRounding = yRounding;
            }
        }
        protected Stack<clippingCommand> _clippingCommandsStack = new Stack<clippingCommand>();

        public DrawingCommandQueue(TouchScreenInterface form, TouchTcpClient tcp)
        {
            _form = form;
            _tcp = tcp;

            // Start a new thread that waits for items to be added to the queue.
            Thread t = new Thread(new ThreadStart(worker));
            t.Start();
        }

        void worker()
        {
            while (true)
            {
                // Wait for an item to be added to the queue or to be canceled.
                _event.WaitOne();

                // Check if canceled while waiting.
                if (_canceled)
                    return; // Gracefully end the thread.

                bool processed = true;

                while (_canceled == false && _queue.Count > 0 && processed)
                {
                    processed = true; // default processed to true before processing each command.

                    MessageReceivedEventArgs e = null;
                    try
                    {
                        // Get the 1st item, but don't remove it yet.
                        e = _queue.Peek();

                        System.Diagnostics.Debug.WriteLine(DateTime.Now.ToString() + "  **** TouchClient Processing queue item: " + ((TouchServiceTcpCommunicationPayloadTypes)e.PayloadType).ToString());

                        switch ((TouchServiceTcpCommunicationPayloadTypes)e.PayloadType)
                        {
                            case TouchServiceTcpCommunicationPayloadTypes.StartDrawing:

                                RendererStartDrawing startDrawing = new RendererStartDrawing(e.Payload);

                                Console.WriteLine(DateTime.Now.ToString() + "  **** TouchClient processing StartDrawing. Mode:" + startDrawing.Bounds);

                                // TODO: add support for rounding... http://www.switchonthecode.com/tutorials/csharp-creating-rounded-rectangles-using-a-graphics-path
                                Region oldRegion = _form.Graphics.Clip;
                                _form.Graphics.Clip = new Region(startDrawing.Bounds);
                                if (oldRegion != null)
                                    oldRegion.Dispose();

                                _clippingCommandsStack.Push(new clippingCommand(_form.Graphics.Clip.Clone(), PushClippingAreaMode.Set, startDrawing.Bounds, startDrawing.XRounding, startDrawing.YRounding));

                                break;

                            case TouchServiceTcpCommunicationPayloadTypes.EndDrawing:

                                Console.WriteLine(DateTime.Now.ToString() + "  **** TouchClient processing EndDrawing");

                                _clippingCommandsStack.Clear();

                                // force the form to redraw the buffered image.
                                _form.InvalidateFormOnFormThread();

                                break;

                            case TouchServiceTcpCommunicationPayloadTypes.ScreenChange:

                                RendererScreenChange screenChange = new RendererScreenChange(e.Payload);

                                Console.WriteLine(DateTime.Now.ToString() + "  **** TouchClient processing ScreenChange. X=" + screenChange.Bounds.X + ", Y=" + screenChange.Bounds.Y + ", Width=" + screenChange.Bounds.Width + ", Height=" + screenChange.Bounds.Height);

                                lock (_screenChanges_lock)
                                {
                                    // Add the screenchange bounds to the list
                                    _screenChanges.Add(screenChange.Bounds);

                                    // Delay the screen change area request so we can optimize the request based on more incoming requests.
                                    if (_screenChangeTimer == null)
                                    {
                                        System.Threading.TimerCallback timerDelegate = new System.Threading.TimerCallback(_screenChangeTimer_Elapsed);
                                        _screenChangeTimer = new System.Threading.Timer(timerDelegate, null, 125, System.Threading.Timeout.Infinite);
                                    }
                                }

                                break;

                            case TouchServiceTcpCommunicationPayloadTypes.PushClippingArea:

                                RendererPushClippingArea pushClippingArea = new RendererPushClippingArea(e.Payload);

                                Console.WriteLine(DateTime.Now.ToString() + "  **** TouchClient processing PushClippingArea. Mode:" + pushClippingArea.Mode + ", Bounds: " + pushClippingArea.Bounds);

                                _clippingCommandsStack.Push(new clippingCommand(_form.Graphics.Clip.Clone(), pushClippingArea.Mode, pushClippingArea.Bounds, pushClippingArea.XRounding, pushClippingArea.YRounding));

                                // TODO: add support for rounding... http://www.switchonthecode.com/tutorials/csharp-creating-rounded-rectangles-using-a-graphics-path
                                switch (pushClippingArea.Mode)
                                {
                                    case PushClippingAreaMode.Set:
                                        Region oldRegion2 = _form.Graphics.Clip;
                                        _form.Graphics.Clip = new Region(pushClippingArea.Bounds);
                                        if (oldRegion2 != null)
                                            oldRegion2.Dispose();
                                        break;
                                    case PushClippingAreaMode.Intersect:
                                        _form.Graphics.Clip.Intersect(pushClippingArea.Bounds);
                                        break;
                                    case PushClippingAreaMode.Exclude:
                                        _form.Graphics.Clip.Exclude(pushClippingArea.Bounds);
                                        break;
                                }
                                
                                break;

                            case TouchServiceTcpCommunicationPayloadTypes.PopClippingArea:

                                Console.WriteLine(DateTime.Now.ToString() + "  **** TouchClient processing PopClippingArea");

                                clippingCommand cc = _clippingCommandsStack.Pop();
                                _form.Graphics.Clip = cc.PreviousClip;

                                break;

                            case TouchServiceTcpCommunicationPayloadTypes.FillRectangle:

                                RendererFillRectangle fillRect = new RendererFillRectangle(e.Payload);
                                using (Brush brush = new SolidBrush(fillRect.Color))
                                    _form.Graphics.FillRectangle(brush, fillRect.Bounds);

                                break;

                            case TouchServiceTcpCommunicationPayloadTypes.FillLinearGradientRectangle:

                                RendererFillLinearGradientRectangle fillLinearGradientRect = new RendererFillLinearGradientRectangle(e.Payload);

                                using (Brush brGradient = new System.Drawing.Drawing2D.LinearGradientBrush(fillLinearGradientRect.Bounds, fillLinearGradientRect.Color1, fillLinearGradientRect.Color2, fillLinearGradientRect.Angle, false))
                                    _form.Graphics.FillRectangle(brGradient, 10, 10, 200, 200);
                                break;

                            case TouchServiceTcpCommunicationPayloadTypes.DrawText:

                                RendererDrawText drawText = new RendererDrawText(e.Payload);

                                System.Diagnostics.Debug.WriteLine(DateTime.Now.ToString() + "  **** TouchClient processing DrawText: " + drawText.Text);

                                using (SolidBrush brush = new SolidBrush(drawText.Color))
                                {
                                    using (StringFormat sf = new StringFormat())
                                    {
                                        sf.Alignment = drawText.HorizontalAlign;
                                        sf.LineAlignment = drawText.VerticalAlign;
                                        sf.Trimming = drawText.Trimming;
                                        sf.FormatFlags = drawText.FormatFlags;
                                        float tabSize = drawText.Font.Size * 4;
                                        sf.SetTabStops(0, new float[] { tabSize, tabSize, tabSize, tabSize, tabSize, tabSize, tabSize, tabSize, tabSize, tabSize });

                                        // Set clipping region so we don't draw outside of the specified area (since this could be inside a j9ControlContainer).
                                        //Region restoreRegion = _form.Graphics.Clip;
                                        //e.Graphics.Clip = new Region(ownerPanelClipRectangle); //maybe there is a way to inherently set up a clipping range within DrawString(), but I don't know how... so rememeber the Clip Region, then set it to the size of the control, draw the image, and restore the clip region.

                                        _form.Graphics.DrawString(drawText.Text, drawText.Font, brush, drawText.Bounds, sf);
                                        //e.Graphics.Clip = restoreRegion; //restore the clip region to what it was before.
                                    }
                                }
                                break;

                            case TouchServiceTcpCommunicationPayloadTypes.DrawImage:

                                using (RendererDrawImage drawImagePayload = new RendererDrawImage(e.Payload))
                                {
                                    Console.WriteLine(DateTime.Now.ToString() + "  **** TouchClient processing DrawImage payload for bounds: X=" + drawImagePayload.Bounds.X + ", Y=" + drawImagePayload.Bounds.Y + ", Width=" + drawImagePayload.Bounds.Width + ", Height=" + drawImagePayload.Bounds.Height);

                                    drawImage(drawImagePayload.Bounds, drawImagePayload.Opacity, drawImagePayload.SizeMode, drawImagePayload.Image);
                                }

                                if (_tcp.RenderingMode == RenderingMode.Snapshots) // snapshot mode doesn't receive an EndDrawing command.
                                    _form.InvalidateFormOnFormThread();
                                break;

                            case TouchServiceTcpCommunicationPayloadTypes.DrawNamedImage:

                                // Also compare the image hash to make sure we have the most recent one.
                                RendererDrawNamedImage drawNamedImage = new RendererDrawNamedImage(e.Payload);

                                Console.WriteLine(DateTime.Now.ToString() + "  **** TouchClient - processing DrawNamedImage queue item. Path: " + drawNamedImage.ImagePath + ", hash: " + drawNamedImage.ImageHash);

                                if (_requestedImagePath != null)
                                {
                                    System.Diagnostics.Debug.WriteLine(DateTime.Now.ToString() + "  **** TouchClient - waiting for a named image so the DrawNamedImage command will not be processed.");
                                    processed = false;
                                    break;
                                }

                                Image img = ImageCache.GetImage(drawNamedImage.ImagePath, drawNamedImage.ImageHash);
                                if (img != null)
                                {
                                    System.Diagnostics.Debug.WriteLine(DateTime.Now.ToString() + "  **** TouchClient - Named Image was in cache: " + drawNamedImage.ImagePath + ", hash: " + drawNamedImage.ImageHash);

                                    // Ensure the Named Image Request timer is stopped. (if it's an http image then the timer won't have been set)
                                    if (_namedImageRequestTimer != null)
                                        _namedImageRequestTimer.Change(System.Threading.Timeout.Infinite, System.Threading.Timeout.Infinite); // stop the timer

                                    drawImage(drawNamedImage.Bounds, drawNamedImage.Opacity,drawNamedImage.SizeMode, img);
                                }
                                else
                                {
                                    System.Diagnostics.Debug.WriteLine(DateTime.Now.ToString() + "  **** TouchClient - Named Image was NOT in cache. Requesting: " + drawNamedImage.ImagePath + "  (expected hash: " + drawNamedImage.ImageHash + ")");

                                    _requestedImagePath = drawNamedImage.ImagePath;

                                    // The named image is not in the cache so request it.
                                    _tcp.SendMessage(new RequestNamedImagePayload(drawNamedImage.ImagePath));

                                    // Start the named image request timeout
                                    System.Threading.TimerCallback timerDelegate = new System.Threading.TimerCallback(_namedImageRequestTimer_Elapsed);
                                    _namedImageRequestTimer = new System.Threading.Timer(timerDelegate, null, 30000, System.Threading.Timeout.Infinite);

                                    // Indicate that this command was not processed so it should not be removed from the queue and we should stop processing queue items.
                                    processed = false; 
                                }
                                break;

                            case TouchServiceTcpCommunicationPayloadTypes.DrawBorder:

                                RendererDrawBorder drawBorder = new RendererDrawBorder(e.Payload);
                                if (drawBorder.Style != ButtonBorderStyle.None)
                                {
                                    Color color = drawBorder.Color;
                                    int width = drawBorder.Width;
                                    ButtonBorderStyle style = drawBorder.Style;
                                    ControlPaint.DrawBorder(_form.Graphics, drawBorder.Bounds, color, width, style, color, width, style, color, width, style, color, width, style);
                                }
                                break;
                            
                            case TouchServiceTcpCommunicationPayloadTypes.DrawLine:

                                RendererDrawLine drawLine = new RendererDrawLine(e.Payload);
                                using (Pen pen = new Pen(drawLine.Color))
                                    _form.Graphics.DrawLine(pen, drawLine.Pt1.X, drawLine.Pt1.Y, drawLine.Pt2.X, drawLine.Pt1.Y);
                                break;
                        }
                    }
                    catch (Exception ex)
                    {
                        System.Diagnostics.Debug.WriteLine(DateTime.Now.ToString() + "  **** TouchClient - EXCEPTION in worker: " + ex.Message);
                        //Logger.Log("An unexpected error occurred while processing the response" + Utility.GetExceptionMessages(ex), LoggerSeverity.Error);

                        //_tcp.SendErrorMessage(messageID, e.Payload, Utility.GetExceptionMessages(ex));
                    }
                    finally
                    {
                        // If the item was processed them dequeue it.
                        if (processed)
                            _queue.Dequeue();
                    }
                }

                // Check if canceled while processing
                if (_canceled)
                    return; // Gracefully end the thread.
            }
        }

        void _namedImageRequestTimer_Elapsed(Object stateInfo)
        {
            // The requested named image was not received within the designated amount of time.

            try
            {
                if (NamedImageRequestTimeout != null)
                    NamedImageRequestTimeout(this, new EventArgs());
            }
            catch
            {
            }
        }

        private readonly object _screenChanges_lock = new object();
        void _screenChangeTimer_Elapsed(Object stateInfo)
        {
            // Get the current list.
            List<Rectangle> screenChanges = _screenChanges;

            lock (_screenChanges_lock)
            {
                // Create a new list for incoming requests.
                _screenChanges = new List<Rectangle>();
                _screenChangeTimer.Dispose();
                _screenChangeTimer = null;
            }

            string s = "";

            // TODO: Try to optimize the screen change requests based on bounds. Right now it just gets a big rectangle that includes all changes.
            Rectangle r = Rectangle.Empty;
            foreach (Rectangle rect in screenChanges)
            {
                if (r.IsEmpty)
                    r = rect;
                else
                    r = Rectangle.Union(r, rect);
            }

            if (r.IsEmpty)
            {
                Console.WriteLine(DateTime.Now.ToString() + "  >>>> TouchClient skipping snapshot request since the region was empty.");
                return;
            }

            Console.WriteLine(DateTime.Now.ToString() + "  >>>> TouchClient requesting snapshot for X=" + r.X + ", Y=" + r.Y + ", Width=" + r.Width + ", Height=" + r.Height);

            // Request the screen snapshot.
            _tcp.RequestSnapshot(r);
        }

        void drawImage(Rectangle bounds, float opacity, ImageSizeMode sizeMode, Image img)
        {
            Rectangle rectSource;
            Rectangle rectDest;
            ImageSizeModeHelper.ImageRectangleFromSizeMode(img, bounds, sizeMode, out rectSource, out rectDest);

            
            

            ImageAttributes ia = null;
            if (opacity != 1)
            {
                ColorMatrix cm = new ColorMatrix();
                cm.Matrix33 = opacity; // Opacity percentage: 0F to 1F
                ia = new ImageAttributes();
                ia.SetColorMatrix(cm);
            }

            _form.Graphics.DrawImage(img, rectDest, rectSource.X, rectSource.Y, rectSource.Width, rectSource.Height, GraphicsUnit.Pixel, ia);

            //e.Graphics.Clip = restoreRegion; //restore the clip region to what it was before.
        }

        public void Enqueue(MessageReceivedEventArgs eventArg)
        {
            _queue.Enqueue(eventArg);

            // Let the worker thread know there's a new item in the queue.
            _event.Set();
        }

        public int Count
        {
            get
            {
                return _queue.Count;
            }
        }

        public string DrawingState
        {
            get
            {
                if (_queue.Count == 0)
                    return "Idle";
                else if (_requestedImagePath != null)
                    return "Requested Image: " + _requestedImagePath;
                else
                    return "Drawing";
            }
        }

        public void NamedImageReceived(string namedImagePath)
        {
            if (_requestedImagePath == namedImagePath)
                _requestedImagePath = null;

            _event.Set();
        }

        public void Cancel()
        {
            // This causes the worker thread to exit.
            _canceled = true;
            _event.Set();
        }

        #region IDisposable Members

        public void Dispose()
        {
            Cancel(); // releases the worker thread.

            if (_namedImageRequestTimer != null)
            {
                _namedImageRequestTimer.Dispose();
                _namedImageRequestTimer = null;
            }
        }

        #endregion
    }
}

